/* 
1、先实现数据劫持，就是给对象的每个属性添加get与set，这是响应式的前提
2、实现深度劫持 无论传入的对象有没有嵌套 都需要给对应的属性添加劫持功能
3、实现数据代理，就是将传入进来的对象中的data中的值添加到vm实例对象上
4、模板编译，准备模板编译的函数，创建文档碎片将所在区域子节点添加到文档碎片
5、递归实现子节点查找到最根本的文本节点，实现文本节点的基本数据替换，基本数据类型和复杂类型都需要实现可以替换
6、添加订阅发布模式，将订阅者与dom节点相关联，相当于监视每一个需要更新数据的dom节点
7、使用发布者收集所有订阅者，数据变化则更新实现响应式
        new Vue的时候data中的数据渲染到页面中则开启监视
        创建订阅者，收集订阅者则将watcher添加到dep的subs当中
        这样数据发生变化，调用notify()才会实现页面更新
8、实现v-model单项数据绑定，标签添加v-model='msg'的属性将内容替换为真正的数据,对当前单向数据呈现的标签 实现一个订阅者 监视
*/


// 先写构造函数
function Vue(options) {
    this.$data = this._data = options.data

    //1、调用函数， 实现数据劫持，给对象的每个属性都添加一个get和set
    observe(this.$data)

    // 2、实现代理
    Object.keys(this.$data).forEach(key => {
        this.proxy(key)
    })

    // 3、模板编译 插值表达式
    Compile(options.el, this)
}



// 这个函数用于实现代理
Vue.prototype = {
    proxy: function (key) {
        // 在构造函数的原型对象的方法中，this还是指向的构造函数的实例对象
        Object.defineProperty(this, key, {
            get() {
                // 返回原来默认值
                return this.$data[key]
            },
            set(value) {
                // 修改之后新值覆盖原值
                this.$data[key] = value
            }
        })
    }
}

// 这个函数用来做数据劫持
function observe(obj) {
    //6. 判断传入的数据是否是对象,条件结束，不向下执行了,递归终止条件
    if (!obj || typeof obj !== 'object') return

    // 创建发布者实例对象
    var dep = new Dep()

    //1. 获取传入的数据，遍历传进来的数据
    Object.keys(obj).forEach(key => {

        // 3. 获取当前属性原来的值，保存到一个变量中
        let oldValue = obj[key]

        // 5.进行递归调用实现深度劫持(对象中传递对象)
        observe(oldValue)

        // 2.给每个属性添加get和set方法
        Object.defineProperty(obj, key, {

            // 4.给每个属性的get和set函数中添加代码实现劫持
            get() {
                Dep.target && dep.addSub(Dep.target)  // 收集订阅者
                return oldValue
            },
            set(value) {
                // 修改触发set，会被重新赋值
                oldValue = value

                // 需要对重新赋值的对象形式的值，也要劫持
                observe(oldValue)

                // 只要一修改值，就会走set中的业务逻辑，也就意味着数据发生了变化，就要让Dom节点进行更新
                dep.notify()
            }
        })
    })
}

// 这个函数用来编译模板
function Compile(el, vm) {
    // 先获取到vm管理的标签对象
    if (typeof el === 'string') {
        vm.$el = document.querySelector(el)
    } else {
        vm.$el = el
    }

    // 创建文档碎片
    var fragment = document.createDocumentFragment()
    var childNode

    // 遍历子节点挨个剪切下来，添加到文档碎片中
    while (childNode = vm.$el.firstChild) {
        // 获取子节点添加到文档碎片中
        fragment.appendChild(childNode)
    }

    // 对文档碎片中的模板替换
    replace(fragment)

    // 将文档碎片中的内容挂载到真正的Dom树上，但是在这之前需要对内容进行替换，不然数据无法渲染
    vm.$el.appendChild(fragment)


    // 这个函数用来进行模板替换
    function replace(node) {
        // 当进行模板替换的时候其实是将文档碎片中的文本内容进行替换
        // 把{{}}中的内容替换成data中数据的真正值

        // 递归的结束条件，如果找到文本节点表示没有子节点了
        if (node.nodeType === 3) {
            // 获取到文本节点内容，通过正则提取需要的内容
            var txtContent = node.textContent //  node.nodeValue

            var regMustache = /\{\{\s*(\S+)\s*\}\}/
            var regResult = regMustache.exec(txtContent)
            if (regResult) {
                // 根据内容进行替换，如果是空则不执行不需要替换，将拿到的变量替换成对应的内容

                // 获取插值表达式中属性对应的值
                // var sourceValue = vm[regResult[1]]
                // 实现插值表达式的数据替换
                // node.textContent = txtContent.replace(regResult[0], sourceValue)

                // 获取插值表达式中属性对应的值，针对复杂数据类型进行穷举
                var sourceValue = regResult[1].split('.').reduce((newObj, key) => newObj[key], vm)
                // 实现插值表达式的数据替换
                node.textContent = txtContent.replace(regResult[0], sourceValue)

                // 将需要替换数据的节点进行监视，这个节点就是订阅者，根据数据更新
                new Watcher(vm, regResult[1], newValue => {
                    // 在这个回调函数中进行真正的数据更新业务
                    node.textContent = txtContent.replace(regResult[0], newValue)
                })

            }
            return // 结束递归

        }

        // 双向数据绑定，先实现单向，获取输入框标签对象属性并赋值
        if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
            // 条件成立找到的则是input标签，
            // 查找标签上的所有属性节点对象，是一个伪数组的形式
            var attrArr = node.attributes // attributes用来获取标签上的所有的属性节点
            // console.log('attrArr', attrArr)

            // 查找v-model = 'msg'这个属性节点对象
            var attrNode = [].slice.call(attrArr).find(item => item.nodeName === 'v-model')

            // 在通过msg找到真正的数据
            var attrNodeValue = attrNode.nodeValue // 拿到的是属性 'msg'
            var sourceValue = attrNodeValue.split('.').reduce((newObj, key) => newObj[key], vm)

            // 在实现数据的呈现
            node.value = sourceValue

            // 对当前输入框开启监视，也就是创建订阅者，只要数据一变化则更新
            new Watcher(vm, attrNodeValue, newValue => {
                node.value = newValue
            })

            // 双向数据绑定，输入框的值变化data中的对应属性也更改，这时候是赋值操作，不是访问属性操作
            // 注册事件，数据变化就触发
            node.addEventListener('input', e => {
                // 找到待更新属性所在的对象,此时是给属性赋值
                var attrNodeArr = attrNodeValue.split('.') // ['user','info','address']
                var attrObj = attrNodeArr.slice(0, -1).reduce((newObj, key) => newObj[key], vm)

                // 找到待更新的属性名但是需要和对象拼接在一起
                var leftAttr = attrNodeArr[attrNodeArr.length - 1]
                attrObj[leftAttr] = e.target.value
            })
        }

        // 如果还有子节点则会进入递归
        node.childNodes.forEach(child => replace(child))
    }
}

// 订阅发布模式，用来实现响应式
// 这个函数 定义发布者类
function Dep() {
    // 用来手机订阅者信息的数组
    this.subs = []
}
Dep.prototype = {
    addSub: function (watcher) {
        this.subs.push(watcher)
    },
    notify: function () {
        // this.subs中存储着很多的watcher(订阅者)
        this.subs.forEach(watcher => watcher.update())
    }
}

// 这个函数 定义订阅者类,在当前的代码当中，就表示对应的DOM节点
function Watcher(vm, key, cd) {// key的值: {{msg}} ，cd是一个回调更新之后执行
    this.vm = vm
    this.key = key // 待更新的属性名 msg  
    this.cd = cd

    // 将当前实例添加到Dep的一个属性上，因为Dep这时候相当于一个全局的变量
    Dep.target = this
    // 为了执行get中的方法，主动访问一个属性，然后就可以执行劫持中get中的逻辑
    // 在执行劫持中的get中业务逻辑的时候则可以手机订阅者
    this.key.split('.').reduce((newObj, key) => newObj[key], this.vm)
    Dep.target = null
}

Watcher.prototype = {
    // 这个方法用来更新dom节点数据，只要已调用就会重新更新dom节点中的数据
    update: function () {
        // 调用才cd实现dom节点中的数据真正更新
        // 注意点是要根据最新的数据重新更新节点
        var newValue = this.key.split('.').reduce((newObj, key) => newObj[key], this.vm)
        this.cd(newValue)
    }
}

